
#include <cstdlib>
#include <iostream>
#include <sstream>

#include "arcball.h"
#include "atmatrix.h"
#include "enumtypes.h"

#include "meshdef/mesh.h"

#include "meshdef/rbfMeshDef.h"
#include "meshdef/rbfMeshDef_GL.h"

#include <GL/glut.h>
#include "selection.h"
#include "glDrawFunctions.h"

using namespace std;

int winw = 700, winh = 700; // window size
int xini, yini; // initial mouse position
int buttonpressed; // 
MeshRenderMode rendermode; // mesh render mode
InteractionMode interactionmode; // interaction mode

Point3 grabStart, grabLast;
int id_moving;
bool showQuad = false;
Point3 quad[4]; // coordinates of quadrilateral tool for selection purposes
vector<uint> fixedGroup; // ids of fixed constraints
vector<uint> handleGroup; // ids of handled constraints
bool handleSelection = false;

AMatrix<GLfloat>     sceneT;
AMatrix<GLfloat>     sceneIniT;
ArcBall              arcball;
Selection            selection;

Mesh                 *mesh=0;

RbfMeshDefGl         *deformer=0;
//~ RbfMeshDef           *deformer=0;
vector<Point3>        constraints;

/// time info
float      cfps;
time_t     start, end;
unsigned   frame;
/// Info variables
void* font=GLUT_BITMAP_TIMES_ROMAN_24;
void* fontsmall=GLUT_BITMAP_HELVETICA_12;
string nvertices("Vertices: ");
string nfaces("Faces: ");
string fps("FPS: ");
string options("Options: p/w/h/l/f/s/+/-");
string mode("Mode: Viewing");

/// Renders info about the model
void renderInfo();
/// Returns the translation vector in world coordinates of two points in screen space
Vector3 screenToWorld(int xi, int yi, int xe, int ye);
/// Returns the world coordinates of a point in screen space
Point3 screenToWorld(int x, int y);
/// Implements the "zoomin" operation. Narrows the view of the scene.
void zoomIn();
/// Implements the "zoomout" operation. Enlarges the view of the scene.
void zoomOut();

void drawQuad();

/// OpenGL display function
void display(void) {
   
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
   glMatrixMode(GL_PROJECTION); /// !!!!!!!!!!!
   glLoadIdentity(); /// !!!!!!!!!!!
   glOrtho(-1,1,-1,1,-4,4); /// !!!!!!!!!!!
   
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glViewport (0, 0, (GLsizei) winw, (GLsizei) winh); /// !!!!!!!!!!!
   
   glMultMatrixf(&sceneT);
   
   /// OpenGL vertex arrays setup
   glDrawModel(mesh, rendermode);
   
   glColor4f(1,0,0,0);
   glPointSize(20);
   glDrawPositions(constraints);
   
   if (showQuad) drawQuad();
   
   /// FPS info
   frame++;
   std::time(&end);
   double ddifftime = std::difftime(end, start);
   if (ddifftime > 1) {
      cfps = frame/ddifftime;
      start = end;
      frame = 0;
      ostringstream s1;
      s1<<cfps;
      fps = "FPS: " + s1.str();
   }
   
   glPushMatrix();
   glLoadIdentity();
   renderInfo();
   glPopMatrix();
   
   glutSwapBuffers();
}

/// Mouse press function
void mouseclick(int button, int state, int x, int y) {
   
   id_moving = -1;
   if (state == GLUT_DOWN) {
      xini=x; yini=winh-y;
      buttonpressed = button;
      if (buttonpressed == GLUT_LEFT_BUTTON 
         and interactionmode == VIEW_MODE) {
         quad[0]=quad[1]=quad[2]=quad[3]=screenToWorld(xini,yini);
         showQuad = true;
      }
      else if (buttonpressed == GLUT_LEFT_BUTTON and 
         interactionmode == DEFORMATION_MODE) {
         
         std::list<unsigned int> selected;
         selection.begin(x, winh-y);
         glDrawPositions(constraints);
         selection.end(selected);
         if (not selected.empty()) id_moving = selected.front();
         
         grabLast = grabStart = screenToWorld(x, winh-y);
      }
      else if (buttonpressed == GLUT_MIDDLE_BUTTON) {
         arcball.click(xini,yini);
      }
      else if (buttonpressed == GLUT_RIGHT_BUTTON) {
         grabLast = grabStart = screenToWorld(x, winh-y);
      }
      sceneIniT = sceneT;
   }
   else if (state == GLUT_UP) {
      if (buttonpressed == GLUT_LEFT_BUTTON 
         and interactionmode == VIEW_MODE) {
         
         y = winh-y;
         std::list<unsigned int> selected;
         if (abs(xini-x)<4 or abs(yini-y)<4) {
            cout<<"ponto!!!"<<endl;
            selection.begin(x, y);
            glDrawModelFaces(mesh);
            selection.end(selected);
            // Include the object being altered
            if (not selected.empty()) {
               unsigned idf = selected.front();
               int id = mesh->faces[idf*3];
               const Point3 p = mesh->positions[id];
               constraints.push_back(p);
            }
         }
         else {
            selection.begin((int)(xini+0.5*(x-xini)), (int)(yini+0.5*(y-yini)), abs(x-xini), abs(y-yini));
            glDrawModelPoints(mesh);
            selection.endAll(selected);
            
            std::list<unsigned int>::iterator it = selected.begin();
            while (it != selected.end()) {
               unsigned idf = *it++;
               cout<<"idf: "<<idf<<endl;
               const Point3 p = mesh->positions[idf];
               // before the push to get the constraints id (size())
               if (handleSelection) 
                  handleGroup.push_back(constraints.size());
               else
                  fixedGroup.push_back(constraints.size());
               constraints.push_back(p);
            }
         }
      }
      showQuad = false;
      handleSelection = false;
   }
   glutPostRedisplay();
}

AMatrix<float> mT; /// an auxiliar matrix
/// Mouse move function
void mousemove(int x, int y) {
   
   if (buttonpressed == GLUT_LEFT_BUTTON 
      and interactionmode == VIEW_MODE) {
      quad[1] = screenToWorld(x,yini);
      quad[2] = screenToWorld(x,winh-y);
      quad[3] = screenToWorld(xini,winh-y);
   }
   else if (buttonpressed == GLUT_LEFT_BUTTON and 
      interactionmode == DEFORMATION_MODE and id_moving >= 0) {
      Point3 grabEnd = screenToWorld(x, winh-y);
      Vector3 t = grabEnd-grabLast;
      constraints[id_moving] = constraints[id_moving] + t;
      //~ constraints.translate(handleGroup, t);
      deformer->execute_deformation(constraints);
      
      grabLast = grabEnd;
   }
   else if (buttonpressed == GLUT_MIDDLE_BUTTON) { // rotation handler
      arcball.drag(x, winh-y, &mT);
      sceneT = mT*sceneIniT;
   }
   else if (buttonpressed == GLUT_RIGHT_BUTTON) { // translation handler
      Vector3 t = screenToWorld(xini, yini, x, winh-y);
      mT.identity();
      mT.translation(t[0], t[1], t[2]);
      //~ sceneT = mT*sceneIniT;
      sceneT = sceneIniT*mT;
   }
   glutPostRedisplay();
}

/// OpenGL key press function
void keypress(unsigned char key, int x, int y) {
   
   switch (key) {
      case 'p':
      case 'P': rendermode = POINTS; break;
      case 'w':
      case 'W': rendermode = WIREFRAME; break;
      case 'h':
      case 'H': rendermode = HIDDENLINES; break;
      case 'l':
      case 'L': rendermode = FLATLINES; break;
      case 'f':
      case 'F': rendermode = FLAT; break;
      case 's':
      case 'S': rendermode = SMOOTH; break;
      
      case 'a':
      case 'A': handleSelection = true; break;
      
      case 'd': { 
         interactionmode = DEFORMATION_MODE;
         if (deformer!=0) delete deformer;
         //~ deformer = new RbfMeshDef(mesh);
         deformer = new RbfMeshDefGl(mesh);
         
         deformer->setting(constraints);
         
         mode = "Mode: Deformation";
         break;
      }
      
      case 't': {
         cout<<"translation ...."<<endl;
         constraints[5] = constraints[5] + Vector3(0.2,0.2,0.2);
         deformer->execute_deformation(constraints);
         cout<<"translation .... end"<<endl;
         break;
      }
      
      case '+': { zoomIn(); break; }
      case '-': { zoomOut(); break; }
      case 27: {
         if (mesh!=0) delete mesh; 
         //~ if (deformer!=0) delete deformer;
         exit(1);
      }
   }
   glutPostRedisplay();
}

void drawQuad() {
   
   glLineStipple(4, 0xAAAA);
   glEnable(GL_LINE_STIPPLE);
   glDisable(GL_LIGHTING);
   glLineWidth(1.0);
   glColor4f(0.0,0.0,0.0,1.0);
   glBegin(GL_LINE_LOOP);
   glVertex3f(quad[0][0], quad[0][1], quad[0][2]);
   glVertex3f(quad[1][0], quad[1][1], quad[1][2]);
   glVertex3f(quad[2][0], quad[2][1], quad[2][2]);
   glVertex3f(quad[3][0], quad[3][1], quad[3][2]);
   glEnd();
   glEnable(GL_LIGHTING);
   glDisable(GL_LINE_STIPPLE);
}

/// auxiliar variables
GLdouble mMtx[16], pMtx[16];  /// modelview/projection matrix
GLint viewport[4];            /// the viewport

/// Returns the world coordinates of a point in screen space
Point3 screenToWorld(int x, int y) {
   
   glGetIntegerv(GL_VIEWPORT, viewport);
   glGetDoublev(GL_PROJECTION_MATRIX, pMtx);
   glGetDoublev(GL_MODELVIEW_MATRIX, mMtx);
   
   GLdouble xw, yw, zw;
   gluUnProject(x, y, 0.0, mMtx, pMtx, viewport, &xw, &yw, &zw);
   return Point3(xw, yw, zw);
}

/// Returns the translation vector in world coordinates of two points in screen space
Vector3 screenToWorld(int xi, int yi, int xe, int ye) {
   
   glGetIntegerv(GL_VIEWPORT, viewport);
   glGetDoublev(GL_PROJECTION_MATRIX, pMtx);
   glGetDoublev(GL_MODELVIEW_MATRIX, mMtx);
   
   GLdouble ws[3], we[3];
   gluUnProject(xi, yi, 0, mMtx, pMtx, viewport, &ws[0], &ws[1], &ws[2]);
   gluUnProject(xe, ye, 0, mMtx, pMtx, viewport, &we[0], &we[1], &we[2]);
   
   return Vector3(we[0]-ws[0], we[1]-ws[1], we[2]-ws[2]);
}

/// OpenGL reshape funcion
void reshape (int w, int h) {
   
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glOrtho(-1,1,-1,1,-4,4);
   
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   winw = w; winh = h;
   glViewport (0, 0, (GLsizei) w, (GLsizei) h);
   glutPostRedisplay();
   
   arcball.setBounds(winw, winh);
}

/// OpenGL initializations
void init(void) {
   
   glEnable(GL_LIGHTING);
   glEnable(GL_LIGHT0);
   glEnable(GL_LIGHT1);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_NORMALIZE);
   
   // lighting setup
   GLfloat left_light_position[] =  {0.0f, 2.0f, 2.0f, 0.0f};
   GLfloat right_light_position[] = {0.0f, -2.0f, 2.0f, 0.0f};
   GLfloat left_diffuse_light[] =  {1.0f, 1.0f, 1.0f, 1.0f};
   GLfloat right_diffuse_light[] = {1.0f, 1.0f, 1.0f, 1.0f};
   GLfloat color_front[] = {0.5f, 0.5f, 1.0f, 1.0f};
   GLfloat colr_back[] = {1.0f, 0.0f, 0.0f, 1.0f};
   
   glLightfv(GL_LIGHT0, GL_POSITION, left_light_position);
   glLightfv(GL_LIGHT1, GL_POSITION, right_light_position);
   glLightfv(GL_LIGHT0, GL_DIFFUSE, left_diffuse_light);
   glLightfv(GL_LIGHT1, GL_DIFFUSE, right_diffuse_light);
   glMaterialfv(GL_FRONT, GL_DIFFUSE, color_front);
   glMaterialfv(GL_BACK, GL_DIFFUSE, colr_back);
   
   glClearColor(1,1,1,1);
   glColor4f(1,1,0,1);
   rendermode = SMOOTH;
   interactionmode = VIEW_MODE;
   
   arcball.setBounds(winw, winh);
   sceneT.identity();
   
   std::time(&start);
   frame = 0;
   
   /// OpenGL vertex arrays setup
   glEnableClientState(GL_VERTEX_ARRAY);
   glEnableClientState(GL_NORMAL_ARRAY);
   glVertexPointer(3, GL_FLOAT, sizeof(Point3), &(mesh->positions[0]));
   glNormalPointer(GL_FLOAT, sizeof(Vector3), &(mesh->normals[0]));
}

/// Implements the "zoomin" operation. Narrows the view of the scene.
void zoomIn() {
   
   AMatrix<float> mT;
   mT.identity();
   mT.scaling(1.2);
   sceneT = mT*sceneT;
}

/// Implements the "zoomout" operation. Enlarges the view of the scene.
void zoomOut() {
   
   AMatrix<float> mT;
   mT.identity();
   mT.scaling(1.0/1.2);
   sceneT = mT*sceneT;
}

/// Renders a string
void renderBitmapString(float x, float y, float z, void *font, const char *string) {
   
   const char *c;
   glRasterPos3f(x, y,z);
   for (c=string; *c != '\0'; c++) {
      glutBitmapCharacter(font, *c);
   }
}

/// Renders info about the model
void renderInfo() {
   
   glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST);
   glColor4f(0,0,0,1);
   renderBitmapString(-0.9,0.90,0,(void *)fontsmall, nvertices.c_str());
   renderBitmapString(-0.9,0.85,0,(void *)fontsmall, nfaces.c_str());
   renderBitmapString(-0.9,0.80,0,(void *)fontsmall, fps.c_str());
   renderBitmapString(-0.9,0.75,0,(void *)fontsmall, options.c_str());
   glEnable(GL_LIGHTING); glEnable(GL_DEPTH_TEST);
}

void printInfoAndExit() {
   
   cout<<endl;
   cout<<"Usage: meshdef filename.off"<<endl;
   exit(0);
}

void open_mesh(string filename) {
   
   string extensao =filename.substr(filename.size()-3,filename.size());
   if (extensao == "off") {
      mesh = Mesh::load_off(filename.c_str());
      ostringstream s1, s2;
      s1<<mesh->nvertices();
      s2<<mesh->nfaces();
      nvertices += s1.str();
      nfaces += s2.str();
   }
   assert(mesh != 0);
}

/// Process the program's parameters
void input(int argc, char **argv) {
   
   int nargs = argc; // 
   if (nargs<2 or nargs>2) printInfoAndExit();
   
   open_mesh(argv[1]);
}

int main(int argc, char **argv) {
   
   // Process the input parameters
   input(argc, argv);
   
   // Glut setup
   glutInit(&argc, argv);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
   glutInitWindowSize(winw, winh);
   glutInitWindowPosition(150, 0);
   glutCreateWindow("RBF mesh deformation prototype");
   
   // OpenGL and data initialization
   init();
   
   // Callbacks registration
   glutDisplayFunc(display);
   glutReshapeFunc(reshape);
   glutMouseFunc(mouseclick);
   glutMotionFunc(mousemove);
   glutKeyboardFunc(keypress);
   glutMainLoop();
   
   return 0;
}
